import { NextFunction, Request, Response } from 'express';
import { JsonWebTokenError, TokenExpiredError, verify } from 'jsonwebtoken';
import { SECRET_KEY } from '@config';
import { DataStoredInToken, RequestWithId } from '@interfaces/auth.interface';
import { logger } from '@utils/logger';
import { saveAuditLog } from '@utils/save-audit-log';
import { prisma, redisToken } from '@/database';
import { normalizeUserData } from '@utils/formatter';
import {
  AccountDisabledException,
  AccountLockedException,
  PermissionException,
  RefreshTokenExpiredException,
  RefreshTokenInvalidException,
  RefreshTokenMissingException,
  TokenExpiredException,
  TokenInvalidException,
  TokenMissingException,
} from '@/exceptions';

const getAuthorization = (req: Request, refresh = false) => {
  const header = req.header('Authorization');
  const refreshHeader = req.header('RefreshToken');
  if (refresh && refreshHeader) return refreshHeader.split('Bearer ')[1];
  if (header) return header.split('Bearer ')[1];

  return null;
};

const whitelist = ['/api-docs/', '/api/auth/login', '/api/auth/signup'];

export const AuthMiddleware = async (req: RequestWithId, res: Response, next: NextFunction) => {
  try {
    if (whitelist.some(path => req.path.includes(path))) return next();

    const Authorization = getAuthorization(req);
    if (!Authorization) return next(new TokenMissingException());

    const { id, username } = verify(Authorization, SECRET_KEY) as DataStoredInToken;
    const findUser = await prisma.user.findFirst({
      where: { id },
      include: {
        department: { select: { id: true, name: true, parent: { select: { id: true, name: true } } } },
        roles: { include: { role: { select: { code: true, name: true } } } },
      },
    });

    if (req.path.includes('refresh')) {
      const refreshToken = getAuthorization(req, true);
      if (!refreshToken) return next(new RefreshTokenMissingException());
      const refreshTokenRedis = await redisToken.get(`${username}:refreshToken`);
      if (!refreshTokenRedis) return next(new RefreshTokenExpiredException());
      if (refreshTokenRedis !== refreshToken) return next(new RefreshTokenInvalidException());
    } else {
      const result = await redisToken.get(`${username}:token`);
      if (!result) return next(new TokenExpiredException());
      if (!findUser || result !== Authorization) return next(new TokenInvalidException());
      if (!findUser.status) return next(new AccountDisabledException());
      if (findUser.passwordErrorCount >= 10) next(new AccountLockedException());
    }
    req.user = normalizeUserData(findUser);
    next();
  } catch (error) {
    logger.error(`[traceId:${req.id}] => ${error}`);
    await saveAuditLog(req, res, error);
    if (error instanceof TokenExpiredError) {
      return next(new TokenExpiredException());
    } else if (error instanceof JsonWebTokenError) {
      return next(new TokenInvalidException());
    }
    next(new PermissionException('权限校验异常'));
  }
};
